﻿using System;
using System.Collections.Generic;
using System.Text;

namespace EmbeddedSharp.Core {

    /// <summary>
    /// Lexer
    /// </summary>
    public class Lexer : ILexer, IDisposable {

        protected string source;

        protected int lineNumber;

        protected Token token;

        protected Token lastToken;

        /// <summary>
        /// Return the current line number
        /// </summary>
        public int LineNumber {
            get {
                return this.lineNumber;
            }
        }

        /// <summary>
        /// Create a new lexer with source
        /// </summary>
        /// <param name="source"></param>
        public Lexer(string source) {
            if (string.IsNullOrEmpty(source)) {
                throw new ArgumentException("source is null");
            }

            this.Index = 0;
            this.source = source;
        }

        /// <summary>
        /// Return the next character
        /// </summary>
        /// <returns></returns>
        public char NextChar() {
            char chr = source[Index++];
            if (chr == '\n' || chr == '\r') {
                lineNumber++;
            }
            if (Index > source.Length) {
                throw new LexerException("End of file");
            }

            return chr;
        }

        /// <summary>
        /// check if there is any tokens remain
        /// </summary>
        /// <returns></returns>
        public bool HasMoreTokens() {
            Token token = PeekToken();
            return token.TokenType != TokenType.End;
            // return this.Index < this.source.Length;
        }

        /// <summary>
        /// current index of lexer
        /// </summary>
        public int Index {
            get;
            protected set;
        }

        /// <summary>
        /// Peek the next token
        /// </summary>
        /// <returns></returns>
        public Token PeekToken() {
            return PeekToken(1);
        }

        /// <summary>
        /// Peek the next nth token
        /// </summary>
        /// <param name="offset">number of tokens to peek</param>
        /// <returns>the resulted token</returns>
        public virtual Token PeekToken(int offset) {
            Token lt = lastToken;
            Token t = token;
            int index = this.Index;

            Token peek = null;
            for (int i = 0; i < offset; i++) {
                peek = NextToken();
            }

            this.Index = index;
            lastToken = lt;
            token = t;

            return peek;
        }

        /// <summary>
        /// Last comment
        /// </summary>
        public string LastComment {
            get;
            set;
        }

        /// <summary>
        /// Returns the next token
        /// </summary>
        /// <returns>next token</returns>
        public virtual Token NextToken() {
            this.lastToken = token;
            token = new Token();

            SkipWhiteSpace();
            if (Index >= this.source.Length) {
                token.TokenType = TokenType.End;
                return token;
            }
            SkipComments();

            char chr = this.source[Index];
            if (char.IsLetter(chr)) {
                token = ReadIdentifier();
            } else if (char.IsNumber(chr)) {
                token = ReadNumber();
            } else {
                token = ReadOperator();
            }

            return token;
        }

        /// <summary>
        /// Read the next operator
        /// </summary>
        /// <returns>the operator</returns>
        public virtual Token ReadOperator() {
            char chr = source[Index];
            StringBuilder buffer = new StringBuilder();

            buffer.Append(chr);
            switch (chr) {
                case '.':
                    token.TokenType = TokenType.Dot;
                    break;
                case '+':
                    token.TokenType = TokenType.Add;
                    break;
                case '-':
                    token.TokenType = TokenType.Sub;
                    break;
                case '*':
                    char nxx = source[Index + 1];
                    if (nxx == '*') {
                        Index++;
                        token.TokenType = TokenType.Square;
                    } else {
                        token.TokenType = TokenType.Mul;
                    }
                    break;
                case '\\':
                    token.TokenType = TokenType.ForwardSlash;
                    break;
                case '/':
                    token.TokenType = TokenType.Div;
                    break;
                case '^':
                    token.TokenType = TokenType.Square;
                    break;
                case '\'':
                    token.TokenType = TokenType.Sqrt;
                    break;
                case ',':
                    token.TokenType = TokenType.Comma;
                    break;
                case ';':
                    token.TokenType = TokenType.Semicolon;
                    break;
                case '(':
                    token.TokenType = TokenType.LeftBrace;
                    break;
                case ')':
                    token.TokenType = TokenType.RightBrace;
                    break;
                case ':':
                    nxx = source[Index + 1];
                    if (nxx == '=') {
                        Index++;
                        token.TokenType = TokenType.Declare;
                    } else {
                        token.TokenType = TokenType.Colon;
                    }
                    break;
                case '&':
                    token.TokenType = TokenType.And;
                    break;
                case '|':
                    token.TokenType = TokenType.Or;
                    break;
                case '!':
                    char nc = source[Index + 1];
                    switch (nc) {
                        case '&':
                            buffer.Append(nc);
                            token.TokenType = TokenType.Nand;
                            Index++;
                            break;
                        case '|':
                            buffer.Append(nc);
                            token.TokenType = TokenType.Nor;
                            Index++;
                            break;
                        case '=':
                            buffer.Append(nc);
                            token.TokenType = TokenType.NotEqual;
                            Index++;
                            break;
                        default:
                            token.TokenType = TokenType.Not;
                            break;
                    }
                    break;
                case '<':
                    if (source[Index + 1] == '=') {
                        Index++;
                        buffer.Append(source[Index]);
                        token.TokenType = TokenType.LessEqualTo;
                    } else {
                        token.TokenType = TokenType.LessThan;
                    }
                    break;
                case '>':
                    if (source[Index + 1] == '=') {
                        Index++;
                        buffer.Append(source[Index]);
                        token.TokenType = TokenType.LargerEqualTo;
                    } else {
                        token.TokenType = TokenType.LargerThan;
                    }
                    break;
                case '[':
                    token.TokenType = TokenType.LeftSquareBrace;
                    break;
                case ']':
                    token.TokenType = TokenType.RightSquareBrace;
                    break;
                case '$':
                    token.TokenType = TokenType.Dollar;
                    break;
                case '{':
                    token.TokenType = TokenType.LeftBracket;
                    break;
                case '}':
                    token.TokenType = TokenType.RightBracket;
                    break;
                case '~':
                    token.TokenType = TokenType.Tilde;
                    break;
                case '@':
                    token.TokenType = TokenType.At;
                    break;
                case '?':
                    token.TokenType = TokenType.Question;
                    break;
                case '_':
                    token.TokenType = TokenType.Underline;
                    break;
                case '=':
                    char nx = source[Index + 1];
                    if (nx == '=') {
                        Index++;
                        buffer.Append(source[Index]);
                        token.TokenType = TokenType.Equal;
                    } else {
                        token.TokenType = TokenType.Assign;
                    }
                    break;
                case '%':
                    token.TokenType = TokenType.Percent;
                    break;
                case '\"':
                    token.TokenType = TokenType.Quote;
                    break;
                case '#':
                    token.TokenType = TokenType.Sharp;
                    break;
                default:
                    throw new UnrecognizedCharacterException(chr);
            }
            Index++;

            token.Value = buffer.ToString();
            return token;
        }

        /// <summary>
        /// Returns the next number
        /// </summary>
        /// <returns></returns>
        public virtual Token ReadNumber() {
            char chr = source[Index];
            StringBuilder buffer = new StringBuilder();

            do {
                buffer.Append(chr);

                Index++;
                if (Index >= this.source.Length) {
                    break;
                }

                chr = this.source[Index];
                if (chr == 'E' || chr == 'e') {
                    buffer.Append(chr);
                    Index++;
                    char nchar = this.source[Index];
                    if (!(nchar == '+' || nchar == '-')) {
                        throw new LexerException("Illegal number format: E or e is not followed by plus or minus.");
                    }

                    buffer.Append(nchar);

                    Index++;
                    if (Index >= this.source.Length) {
                        break;
                    }

                    chr = this.source[Index];
                }
            }
            while (
                (Char.IsDigit(chr) || chr == '.')
                // || chr == 'E' || chr == '-' || chr == '+' || chr == 'e')
                && Index < this.source.Length
                );

            token.TokenType = TokenType.Number;
            token.Value = buffer.ToString();

            return token;
        }

        /// <summary>
        /// Returns the next identifier
        /// </summary>
        /// <returns>the next identitifer</returns>
        public Token ReadIdentifier() {
            char chr = source[Index];
            StringBuilder buffer = new StringBuilder();

            do {
                buffer.Append(chr);
                Index++;
                if (Index >= this.source.Length) {
                    break;
                }
                chr = this.source[Index];
            }
            while (
                (Char.IsLetterOrDigit(chr) || chr == '.' || chr == '_'));

            token.TokenType = TokenType.IDENTIFIER;
            token.Value = buffer.ToString();

            return token;
        }

        /// <summary>
        /// Skip comments
        /// </summary>
        protected virtual void SkipComments() {
            char chr = source[Index];

            while (chr == '#') {
                string cmt = this.ReadUntil('\n', '\r');
                SkipWhiteSpace();
                chr = this.source[Index];
                if (chr == '#') {
                    this.LastComment = this.LastComment + " " + cmt;
                } else {
                    this.LastComment = cmt;
                }
            }
            if (chr == '/'
                && Index + 1 < this.source.Length
                && this.source[Index + 1] == '*') {
                Index = Index + 2;

                StringBuilder buff = new StringBuilder();
                // consumes the comment until the end of it
                while (!(this.source[Index] == '*'
                    && this.Index + 1 < this.source.Length
                    && this.source[Index + 1] == '/')) {
                    char c = this.source[Index];
                    buff.Append(c);
                    Index++;
                }
                if (this.Index == this.source.Length) {
                    throw new Exception();
                }
                this.LastComment = buff.ToString();
                Index = Index + 2;
                SkipWhiteSpace();
                chr = this.source[Index];
            }
        }

        /// <summary>
        /// Skip the whitespace characters
        /// </summary>
        public void SkipWhiteSpace() {
            if (Index >= source.Length) {
                return;
            }

            char chr = source[Index];
            if (char.IsWhiteSpace(chr)) {
                do {
                    Index++;
                    if (Index >= this.source.Length) {
                        break;
                    }
                    chr = this.source[Index];
                }
                while (char.IsWhiteSpace(chr));
            }

            if (Index > this.source.Length) {
                throw new ApplicationException(
                    string.Format(
                    "Character stream is overflow at {0}:{1}",
                    this.source, this.LineNumber));
            }
        }

        /// <summary>
        /// Walk through to the end of the line
        /// </summary>
        public string NextLine() {
            string t = ReadUntil('\r', '\n');
            SkipWhiteSpace();

            return t;
        }

        /// <summary>
        /// Returns the next token, should be one of the expected tokens, othewise warns
        /// </summary>
        /// <param name="expectedToken">the expected tokens</param>
        /// <returns>the next token</returns>
        public Token NextToken(params string[] expectedToken) {
            Token actualToken = this.NextToken();
            foreach (string m in expectedToken) {
                if (actualToken.Value == m) {
                    return actualToken;
                }
            }

            throw new TokenNotAsExpectedException(
                expectedToken, actualToken.Value);
        }

        /// <summary>
        /// Read until the given token is reached
        /// </summary>
        /// <param name="token">the terminal token</param>
        /// <returns>the string in between</returns>
        public string ReadUntil(params string[] token) {
            return string.Empty;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="types"></param>
        /// <returns></returns>
        public string ReadUntil(params TokenType[] types) {
            return string.Empty;
        }

        /// <summary>
        /// Read until the given characters are reached
        /// </summary>
        /// <param name="p">the sequence of terminal characters</param>
        /// <returns>the string in between</returns>
        public string ReadUntil(params char[] p) {
            List<char> v = new List<char>(p);
            StringBuilder b = new StringBuilder();

            char c = source[Index];
            while (!v.Contains(c) && Index < this.source.Length) {
                b.Append(c);
                ++Index;
                if (Index >= this.source.Length) {
                    return b.ToString();
                }
                c = source[Index];
                if (c == '\r' || c == '\n') {
                    this.lineNumber++;
                }
            }
            Index++;
            //if (Index < this.source.Length)
            //{
            //    b.Append(c);
            //}

            return b.ToString();
        }

        /// <summary>
        /// Peek the next character
        /// </summary>
        /// <returns>the next character</returns>
        public char PeekChar() {
            int Index = this.Index;
            char c = (char)0;

            do {
                if (Index < this.source.Length) {
                    c = this.source[Index++];
                } else {
                    break;
                }
            }
            while (char.IsWhiteSpace(c));
            return c;
        }

        public char PreviousChar(int p) {
            return this.source[Index - p];
        }

        public Token LastToken {
            get {
                return lastToken;
            }
        }

        /// <summary>
        /// Get next token
        /// </summary>
        /// <param name="tokenType"></param>
        /// <returns></returns>
        public Token NextToken(params TokenType[] tokenType) {
            Token token = NextToken();
            if (token.TokenType == TokenType.Sub) {
                Token nt = PeekToken();
                if (nt.TokenType == TokenType.Number) {
                    token = NextToken();
                    token.Value = "-" + token.Value;
                }
            }

            foreach (TokenType tt in tokenType) {
                if (token.TokenType == tt) {
                    return token;
                }
            }

            throw new TokenNotAsExpectedException(
                tokenType, token.Value);
        }

        #region IDisposable Members

        public void Dispose() {
        }

        #endregion
    }
}
